跳到主要内容

为什么需要学习 webpack5 module Federation 原理呢?因为EMP 微前端方案正是基于该革命性功能进行的,具有历史突破意义。通过本文,可以让你深入学习 webpack5 module Federation 原理,掌握 EMP 微前端方案的底层基石,更好使用和应用 EMP 微前端方案。

最近 webpack5 正式发布,其中推出了一个非常令人激动的新功能,即今日的主角——Module Federation(以下简称为 mf),下面将通过三个方面(what,how,where)来跟大家一起探索这个功能的奥秘。

一. Module Federation 是什么

Module Federation 中文直译为“模块联邦”,而在 webpack 官方文档中,其实并未给出其真正含义,但给出了使用该功能的 motivation, 即动机,原文如下

Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually.

This is often known as Micro-Frontends, but is not limited to that.

翻译成中文即

多个独立的构建可以形成一个应用程序。这些独立的构建不会相互依赖,因此可以单独开发和部署它们。
这通常被称为微前端,但并不仅限于此。

结合以上,不难看出,mf 实际想要做的事,便是把多个无相互依赖、单独部署的应用合并为一个。通俗点讲,即 mf 提供了能在当前应用中远程加载其他服务器上应用的能力。对此,可以引出下面两个概念:

  • host:引用了其他应用的应用
  • remote:被其他应用所使用的应用 img 鉴于 mf 的能力,我们可以完全实现一个去中心化的应用部署群:每个应用是单独部署在各自的服务器,每个应用都可以引用其他应用,也能被其他应用所引用,即每个应用可以充当 host 的角色,亦可以作为 remote 出现,无中心应用的概念。 img

二. Module Federation 如何使用

配置示例:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
// 其他webpack配置...
plugins: [
new ModuleFederationPlugin({
name: 'empBase',
library: { type: 'var', name: 'empBase' },
filename: 'emp.js',
remotes: {
app_two: "app_two_remote",
app_three: "app_three_remote"
},
exposes: {
'./Component1': 'src/components/Component1',
'./Component2': 'src/components/Component2',
},
shared: ["react", "react-dom","react-router-dom"]
})
]
}

通过以上配置,我们对 mf 有了一个初步的认识,即如果要使用 mf,需要配置好几个重要的属性:

字段名类型含义
namestring必传值,即输出的模块名,被远程引用时路径为${name}/${expose}
libraryobject声明全局变量的方式,name 为 umd 的 name
filenamestring构建输出的文件名
remotesobject远程引用的应用名及其别名的映射,使用时以 key 值作为 name
exposesobject被远程引用时可暴露的资源路径及其别名
sharedobject与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖

三.Module Federation 的构建解析

让我们看看构建后的代码:

var moduleMap = {
"./components/Comonpnent1": function() {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react"), __webpack_require__.e("src_components_Close_index_tsx")]).then(function() { return function() { return (__webpack_require__(16499)); }; });
},
};
var get = function(module, getScope) {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(function() {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
var init = function(shareScope, initScope) {
if (!__webpack_require__.S) return;
var oldScope = __webpack_require__.S["default"];
var name = "default"
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
}

可以看到,代码中包括三个部分:

  • moduleMap:通过 exposes 生成的模块集合
  • get: host 通过该函数,可以拿到 remote 中的组件
  • init:host 通过该函数将依赖注入 remote 中

再看moduleMap,返回对应组件前,先通过__webpack_require__.e加载了其对应的依赖,让我们看看__webpack_require__.e做了什么:

__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function(chunkId) {
// 获取__webpack_require__.f中的依赖
return Promise.all(Object.keys(__webpack_require__.f).reduce(function(promises, key) {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
__webpack_require__.f.consumes = function(chunkId, promises) {
// 检查当前需要加载的chunk是否是在配置项中被声明为shared共享资源,如果在__webpack_require__.O上能找到对应资源,则直接使用,不再去请求资源
if(__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach(function(id) {
if(__webpack_require__.o(installedModules, id)) return promises.push(installedModules[id]);
var onFactory = function(factory) {
installedModules[id] = 0;
__webpack_modules__[id] = function(module) {
delete __webpack_module_cache__[id];
module.exports = factory();
}
};
try {
var promise = moduleToHandlerMapping[id]();
if(promise.then) {
promises.push(installedModules[id] = promise.then(onFactory).catch(onError));
} else onFactory(promise);
} catch(e) { onError(e); }
});
}
}

通读核心代码之后,可以得到如下总结:

  • 首先,mf 会让 webpack 以filename作为文件名生成文件
  • 其次,文件中以 var 的形式暴露了一个名为name的全局变量,其中包含了exposes以及shared中配置的内容
  • 最后,作为host时,先通过remoteinit方法将自身shared写入remote中,再通过get获取remoteexpose的组件,而作为remote时,判断host中是否有可用的共享依赖,若有,则加载host的这部分依赖,若无,则加载自身依赖。

四. Module Federation 的应用场景有哪些

英雄也怕无用武之地,让我们看看 mf 的应用场景有哪些:

  • 微前端:通过 shared 以及 exposes 可以将多个应用引入同一应用中进行管理,由 YY 业务中台 web 前端组团队自主研发的EMP 微前端方案就是基于 mf 的能力而实现的。
  • 资源复用,减少编译体积:可以将多个应用都用到的通用组件单独部署,通过 mf 的功能在 runtime 时引入到其他项目中,这样组件代码就不会编译到项目中,同时亦能满足多个项目同时使用的需求,一举两得。

https://github.com/efoxTeam/emp/wiki/%E3%80%8Amodule-Federation%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0%E3%80%8B